/* ---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

// 部分代码来源并修改自 https://github.com/microsoft/vscode/blob/1.63.0/src/vs/platform/terminal/node/terminalProcess.ts

import { promises } from 'fs';
import os from 'os';

import omit from 'lodash/omit';
import * as pty from 'node-pty';
import * as osLocale from 'os-locale';

import { IShellLaunchConfig, ITerminalLaunchError } from '../common';
import { IProcessExitEvent, IProcessReadyEvent } from '../common';
import { IPtyProcessProxy, IPtyService, IPtySpawnOptions } from '../common';

import { findExecutable } from './shell';
import { IShellIntegrationService } from './shell-integration.service';
import { Disposable, Emitter, isWindows, URI } from '../../utils';
import path from 'path';
import { getShellPath } from '../../utils/shell-path';
import { IPtyServiceManager } from './pty.manager';

export { IPtyService };

// Pty 服务实现，负责管理终端的启动、数据通信、退出等
export class PtyService extends Disposable implements IPtyService {
  // 通过构造函数传入
  protected readonly ptyServiceManager: IPtyServiceManager;
  protected readonly shellIntegrationService: IShellIntegrationService;

  // Pty 启动参数
  protected readonly _ptyOptions:
    | pty.IPtyForkOptions
    | pty.IWindowsPtyForkOptions;
  // Pty 进程代理对象
  private _ptyProcess: IPtyProcessProxy | undefined;

  // 终端数据事件
  private readonly _onData = new Emitter<string>();
  readonly onData = this._onData.event;

  // 终端就绪事件
  private readonly _onReady = new Emitter<IProcessReadyEvent>();
  readonly onReady = this._onReady.event;

  // 终端退出事件
  private readonly _onExit = new Emitter<IProcessExitEvent>();
  readonly onExit = this._onExit.event;

  // 终端 sessionId（构造函数传入的 id）
  private readonly sessionId: string;

  // 进程名变更事件
  private readonly _onProcessChange = new Emitter<string>();
  readonly onProcessChange = this._onProcessChange.event;

  // 上一次的进程名
  private previouslyProcess: string | undefined;

  // 获取当前 pty 进程代理
  get pty() {
    return this._ptyProcess;
  }

  /**
   * 构造函数
   * @param id sessionId
   * @param shellLaunchConfig 启动配置
   * @param cols 列数
   * @param rows 行数
   * @param ptyServiceManager 服务管理器（手动传入）
   * @param shellIntegrationService shell集成服务（手动传入）
   */
  constructor(
    public id: string,
    public readonly shellLaunchConfig: IShellLaunchConfig,
    cols: number,
    rows: number,
    ptyServiceManager: IPtyServiceManager,
    shellIntegrationService: IShellIntegrationService
  ) {

    super();
    this.ptyServiceManager = ptyServiceManager;
    this.shellIntegrationService = shellIntegrationService;
    this.sessionId = id;

    let name: string;
    if (isWindows) {
      name = path.basename(this.shellLaunchConfig.executable || '');
    } else {
      name = 'xterm-256color';
    }

    const cwd = this.parseCwd();
    this._ptyOptions = {
      name,
      cwd,
      env: shellLaunchConfig.env as { [key: string]: string },
      cols,
      rows,
    };
  }

  // 杀死终端进程
  async kill(): Promise<void> {
    if (this.disposed) {
      return;
    }

    try {
      if (this._ptyProcess) {
        this._ptyProcess.kill();
      }
    } catch (ex) {}
  }

  // 校验工作目录是否合法
  async _validateCwd(): Promise<ITerminalLaunchError | undefined> {
    const { cwd } = this._ptyOptions;
    if (cwd) {
      try {
        const result = await promises.stat(cwd);
        if (!result.isDirectory()) {
          return {
            message: `Starting directory (cwd) "${cwd}" is not a directory`,
          };
        }
      } catch (err) {
        if ((err as any)?.code === 'ENOENT') {
          return {
            message: `Starting directory (cwd) "${cwd}" does not exist`,
          };
        }
      }
    } else {
      return {
        message: 'IPtyForkOptions.cwd not set',
      };
    }
  }

  // 校验可执行文件是否合法
  async _validateExecutable(): Promise<ITerminalLaunchError | undefined> {
    const options = this.shellLaunchConfig;
    const { cwd } = this._ptyOptions;
    if (!options.executable) {
      return {
        message: 'IShellLaunchConfig.executable not set',
      };
    }
    try {
      const result = await promises.stat(options.executable);
      if (!result.isFile() && !result.isSymbolicLink()) {
        return {
          message: `Path to shell executable "${options.executable}" is not a file or a symlink`,
        };
      }
    } catch (err) {
      if ((err as any)?.code === 'ENOENT') {
        // 可执行文件不是绝对路径，尝试在 PATH 或 CWD 下查找
        const envPaths: string[] | undefined =
          options.env && options.env.PATH
            ? options.env.PATH.split(path.delimiter)
            : undefined;
        const executable = await findExecutable(
          options.executable,
          cwd,
          envPaths
        );
        if (!executable) {
          return {
            message: `Path to shell executable "${options.executable}" does not exist`,
          };
        }
        // 显式设置可执行文件路径，避免 node-pty 再次查找
        options.executable = executable;
      }
    }
  }

  // 启动终端
  async start(): Promise<ITerminalLaunchError | undefined> {
    const options = this.shellLaunchConfig;

    // 获取本地化环境变量
    const locale = await osLocale.default();
    let ptyEnv: { [key: string]: string };

    if (options.strictEnv) {
      ptyEnv = options.env as { [key: string]: string };
    } else {
      ptyEnv = Object.assign(
        {},
        // 过滤部分 IDE 相关环境变量
        omit(process.env, [
          'KTELECTRON',
          'EXTENSION_HOST_ENTRY',
          'EXTENSION_DIR',
          'WORKSPACE_DIR',
          'CODE_WINDOW_CLIENT_ID',
          'VSCODE_NLS_CONFIG',
          'ELECTRON_RUN_AS_NODE',
        ]),
        {
          LC_ALL: `${locale.replace('-', '_')}.UTF-8`,
          LANG: `${locale.replace('-', '_')}.UTF-8`,
          /**
           * 重要说明：
           * **这里不要使用 shell-path ，因为其依赖的 shell-env 在某些情况下，macOS 及 Linux 上都会出现永远无法返回的问题
           * [shell-env's sync function returns no output](https://github.com/sindresorhus/shell-env/issues/17)
           * 这会导致 IDE 进程的通信直卡住无法返回，表现为假死状态，进程运行正常，但前端任何交互都会一直 loading**
           */
          PATH: await getShellPath(),
        },
        options.env
      ) as { [key: string]: string };
    }

    // HACK: 针对 bash 的集成能力注入
    if (options.executable?.includes('bash')) {
      const bashIntegrationPath =
        await this.shellIntegrationService.initBashInitFile();
      if (!options.args) {
        options.args = [];
      }
      if (Array.isArray(options.args)) {
        // 如果参数中没有 --init-file，则添加
        if (!options.args.includes('--init-file')) {
          // --init-file 必须放在最前面，否则 bash 启动会失败
          options.args.unshift('--init-file', bashIntegrationPath);
        }
      }
    }

    // 针对 zsh 的集成能力注入
    if (options.executable?.includes('zsh')) {
      const zshDotFilesPath =
        await this.shellIntegrationService.initZshDotFiles();

      if (!ptyEnv) {
        ptyEnv = {};
      }

      ptyEnv['USER_ZDOTDIR'] = ptyEnv['ZDOTDIR'] || os.homedir() || '~';
      ptyEnv['ZDOTDIR'] = zshDotFilesPath;
    }

    this._ptyOptions['env'] = ptyEnv;

    // 校验工作目录和可执行文件
    const results = await Promise.all([
      this._validateCwd(),
      this._validateExecutable(),
    ]);
    const firstError = results.find((r) => r !== undefined);
    if (firstError) {
      return firstError;
    }

    try {

      await this.setupPtyProcess();
      return undefined;
    } catch (err) {
      console.log('IPty#spawn native exception', err);
      return {
        message: `A native exception occurred during launch (${
          (err as any)?.message
        })`,
      };
    }
  }

  // 初始化并启动 pty 进程
  protected async setupPtyProcess() {

    const options = this.shellLaunchConfig;
    const ptySpawnOptions: IPtySpawnOptions = {
      preserveHistory: !options?.disablePreserveHistory,
    };
    const args = options.args || [];

    // 通过 ptyServiceManager 启动 pty 进程
    const ptyProcess = await this.ptyServiceManager.spawn(
      options.executable as string,
      args,
      this._ptyOptions,
      this.sessionId,
      ptySpawnOptions
    );

    // 监听终端数据事件
    this.addDispose(
      ptyProcess.onData(async (e) => {
        this._onData.fire(e);
        // 检查进程名是否变化，变化时触发事件
        const processName = await ptyProcess.getProcessDynamically();
        if (processName !== this.previouslyProcess) {
          this.previouslyProcess = processName;
          this._onProcessChange.fire(processName);
        }
      })
    );

    // 监听终端退出事件
    this.addDispose(
      ptyProcess.onExit((e) => {
        this._onExit.fire(e);
        this.dispose();
      })
    );

    // 设置 pty 进程相关属性
    ptyProcess.bin = options.executable as string;
    ptyProcess.launchConfig = options;
    ptyProcess.parsedName = path.basename(options.executable as string);

    this._sendProcessId(ptyProcess.pid);

    this._ptyProcess = ptyProcess;
  }

  // 发送进程 ID
  private _sendProcessId(pid: number) {
    this._onReady.fire({
      pid,
    });
  }

  // 解析工作目录
  protected parseCwd() {
    if (this.shellLaunchConfig.cwd) {
      return typeof this.shellLaunchConfig.cwd === 'string'
        ? this.shellLaunchConfig.cwd
        : URI.from(this.shellLaunchConfig.cwd).path.toString();
    }
    return os.homedir();
  }

  // 获取 shell 名称
  getShellName() {
    return this._ptyProcess?.parsedName || 'not started';
  }

  // 获取进程 ID
  getPid() {
    return this._ptyProcess?.pid || -1;
  }

  // 获取当前工作目录
  async getCwd() {
    if (!this._ptyProcess) {
      return undefined;
    }
    return this._ptyProcess.getCwd();
  }

  // 调整终端尺寸
  resize(rows: number, cols: number) {
    try {
      this._ptyProcess?.resize(cols, rows);
    } catch (e) {
      return false;
    }
    return true;
  }

  // 向终端写入数据
  onMessage(data: string) {
    this._ptyProcess?.write(data);
  }
}
